﻿using EasyNet.Componets.Core.Domain;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace EasyNet.Componets.Data.EntityFramework.Extensions
{
    /// <summary>
    /// 表示基于Entity Framework的一些关于DB Entity的相关扩展
    /// 如Attach游离对象如（缓存的对象)附加到EF的跟踪，使其可以正常更新和添加；获取EF Entity 的相关配置的扩展类型等。
    /// </summary>
    internal static class DbEntityExtensions
    {
        /// <summary>
        /// 根据实体对象，获取EF上下文配置的对应的 实体配置信息
        /// </summary>
        /// <typeparam name="TEntity">实现IEntity接口的类型</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">Entity对象</param>
        /// <returns>EntityType对象</returns>
        internal static EntityType GetEntityType<TEntity>(this DbContext dbContext, TEntity entity)
        {
            //访问EF元数据
            var workspace = ((IObjectContextAdapter)dbContext).ObjectContext.MetadataWorkspace;
            //O-Space 这是对象空间，或称CLR空间。其中存储着元数据中与概念模型映射的CLR类型。
            var itemCollection = (ObjectItemCollection)(workspace.GetItemCollection(DataSpace.OSpace));
            //获取实体类型信息
            var entityType = itemCollection.OfType<EntityType>().Single(e => itemCollection.GetClrType(e) == typeof(TEntity));

            return entityType;
        }

        /// <summary>
        /// 根据实体对象，获取其中的导航属性（既一对多，一对一相关的）
        /// </summary>
        /// <typeparam name="TEntity">实现IEntity接口的类型</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">Entity对象</param>
        /// <returns>导航属性对应的PropertyInfo</returns>
        internal static IList<PropertyInfo> GetEntityNavigationPropertieInfos<TEntity>(this DbContext dbContext, TEntity entity)
            where TEntity : class, IEntity
        {
            var entityType = dbContext.GetEntityType(entity);

            //获取所有实体内的导航信息
            var navigationProperties = entityType.NavigationProperties;

            var propertyInfoList = new List<PropertyInfo>();

            if (navigationProperties != null && navigationProperties.Any())
            {
                //遍历导航属性
                foreach (var property in navigationProperties)
                {
                    var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
                               BindingFlags.NonPublic;

                    //获取聚合根中的导航属性
                    var entityPropertyInfo = typeof(TEntity).GetProperty(property.Name, flags);

                    propertyInfoList.Add(entityPropertyInfo);
                }
            }

            return propertyInfoList;
        }

        /// <summary>
        /// 获取EF实体对应的数据库表名
        /// </summary>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <returns>实体类型对应的数据库表名</returns>
        internal static IDictionary<Type, string> GetEntityTableMaps(this DbContext dbContext)
        {
            var dict = new Dictionary<Type, string>();

            //访问EF元数据
            var workspace = ((IObjectContextAdapter)dbContext).ObjectContext.MetadataWorkspace;
            //S-Space. 这是存储元数据中数据库模型的地方
            var storeItemCollection = (StoreItemCollection)(workspace.GetItemCollection(DataSpace.SSpace));
            //获取实体容器集合，一般只有一个容器(如CodeFirstDatabase)
            var entityContainers = storeItemCollection.GetItems<EntityContainer>();
            //从容器中获取配置的所有数据库模型EntitySet，用于获取表信息
            var entitySets = entityContainers.Single().BaseEntitySets.OfType<EntitySet>().ToList();

            //O-Space 这是对象空间，或称CLR空间。其中存储着元数据中与概念模型映射的CLR类型。
            var objectItemCollection = (ObjectItemCollection)(workspace.GetItemCollection(DataSpace.OSpace));
            //获取概念模型的实体类型
            var entityTypes = objectItemCollection.OfType<EntityType>().ToList();

            //遍历所有配置的实体类型
            foreach (var entityType in entityTypes)
            {
                //查询对应的表查
                var table = entitySets.FirstOrDefault(x => x.Name == entityType.Name);

                //如果表不存在，则实体类没有对应一张表，可能和父类实体共用一张表（如TPH）
                if (table == null)
                {
                    EdmType baseType = entityType;

                    while (table == null)
                    {
                        baseType = baseType.BaseType;
                        table = entitySets.FirstOrDefault(x => x.Name == baseType.Name);
                    }
                }

                //表名
                var tableName = table.MetadataProperties.Contains("Table") &&
                                  table.MetadataProperties["Table"].Value != null
                      ? table.MetadataProperties["Table"].Value.ToString() : table.Name;
                //Schema名称
                var tableSchema = table.MetadataProperties["Schema"].Value.ToString();

                dict.Add(objectItemCollection.GetClrType(entityType),
                    string.Format("{0}.{1}", tableSchema, tableName));
            }

            return dict;
        }

        /// <summary>
        /// 附加一个聚合根对象到EF数据库上下文，使其跟踪。状态类型设置为Unchanged
        /// </summary>
        /// <typeparam name="TAggregateRoot">对象类型，必须实现TAggregateRoot接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="aggregateRoot">聚合根对象</param>
        internal static void AttachAggregateRoot<TAggregateRoot>(this DbContext dbContext,
            TAggregateRoot aggregateRoot) where TAggregateRoot : class, IAggregateRoot
        {
            //附加聚合根
            dbContext.AttachAggregateRoots(new List<TAggregateRoot>() { aggregateRoot });
        }

        /// <summary>
        /// 批量附加聚合根对象到EF数据库上下文，使其跟踪。状态类型设置为Unchanged
        /// </summary>
        /// <typeparam name="TAggregateRoot">对象类型，必须实现TAggregateRoot接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="aggregateRoots">聚合根对象列表</param>
        internal static void AttachAggregateRoots<TAggregateRoot>(this DbContext dbContext,
            IList<TAggregateRoot> aggregateRoots) where TAggregateRoot : class, IAggregateRoot
        {
            if (aggregateRoots != null)
            {
                var isDetectChangeEnabled = dbContext.Configuration.AutoDetectChangesEnabled;
                dbContext.Configuration.AutoDetectChangesEnabled = false;

                try
                {
                    foreach (var aggregateRoot in aggregateRoots)
                    {
                        //附加聚合根
                        dbContext.AttachEntity(aggregateRoot);
                    }
                }
                finally
                {
                    dbContext.Configuration.AutoDetectChangesEnabled = isDetectChangeEnabled;
                }
            }
        }

        /// <summary>
        /// 附加实体对象到EF上下文，使其可以被跟踪。若存在跟踪对象，则分离它。附加传入的实体
        /// </summary>
        /// <typeparam name="TEntity">对象类型，必须实现IEntity接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">需要修改状态的Entity对象</param>
        internal static void AttachEntity<TEntity>(this DbContext dbContext, TEntity entity)
            where TEntity : class, IEntity
        {
            if (dbContext.Set<TEntity>().Local.FirstOrDefault(x => x.Id == entity.Id) != null)
            {
                dbContext.ChangeEntityStateIncludeSubEntity(entity, EntityState.Unchanged);
            }
            else
            {
                dbContext.Entry(entity).State = EntityState.Unchanged;
            }
        }

        /// <summary>
        ///  设置前实体对象为已经修改的状态，若存在跟踪对象，则分离它。附加传入的实体
        /// </summary>
        /// <typeparam name="TEntity">对象类型，必须实现IEntity接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">需要修改状态的Entity对象</param>
        internal static void ChangeEntityModified<TEntity>(this DbContext dbContext, TEntity entity)
            where TEntity : class, IEntity
        {
            //查询本地是否Attach跟踪指定的对象
            var localObject = dbContext.Set<TEntity>().Local.FirstOrDefault(f => f.Id == entity.Id);

            //判断本地跟踪对象是否已经存在, 并且不是同一个
            if (localObject != null && localObject != entity)
            {
                dbContext.DetachedEntity(localObject);
            }

            //附加新的对象
            dbContext.Entry(entity).State = EntityState.Modified;
        }

        /// <summary>
        /// 分离实体对象，使其不受EF上下文跟踪
        /// </summary>
        /// <typeparam name="TEntity">对象类型，必须实现IEntity接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">需要修改状态的Entity对象</param>
        internal static void DetachedEntity<TEntity>(this DbContext dbContext, TEntity entity)
            where TEntity : class, IEntity
        {
            dbContext.ChangeEntityStateIncludeSubEntity(entity, EntityState.Detached);
        }

        /// <summary>
        /// 修改一个对象的实体状态，同时包含实体对象中的子对象, 包括子对象集合，也一并设置为相同的状态
        /// </summary>
        /// <typeparam name="TEntity">对象类型，必须实现IEntity接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">需要修改状态的Entity对象</param>
        /// <param name="entityState">修改对象的新状态</param>
        internal static void ChangeEntityStateIncludeSubEntity<TEntity>(this DbContext dbContext,
            TEntity entity, EntityState entityState)
            where TEntity : class, IEntity
        {
            //获取导航属性
            var propertieInfos = dbContext.GetEntityNavigationPropertieInfos(entity);

            foreach (var entityPropertyInfo in propertieInfos)
            {
                //如果导航属性是泛型，并且实现了ICollection<>接口 【一对多关系】
                if (entityPropertyInfo.PropertyType.IsGenericType &&
                    (entityPropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>)
                    || (Array.Exists(entityPropertyInfo.PropertyType.GetInterfaces(),
                        t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ICollection<>)))
                    ))
                {
                    var objs = entityPropertyInfo.GetValue(entity);
                    IEnumerable<IEntity> subEntityCol = objs as IEnumerable<IEntity>;

                    if (subEntityCol != null && subEntityCol.Any())
                    {
                        foreach (var subEntity in subEntityCol.ToList())
                        {
                            //附加对象，递归调用（以下方法不可用）
                            //ChangeEntityStateIncludeSubEntity(dbContext, subEntity, entityState);

                            //不直接使用ChangeEntityStateIncludeSubEntity方法，而是通过反射调用的原因：
                            //泛型需要一个明确的类型，而subEntity目前只能明确转换为IEntity实例
                            //故而，如果直接调用ChangeEntityStateIncludeSubEntity方法，其中的泛型参数TEntity为变成 IEntity
                            //从而使得最终调用的dbContext.Set<T>() 解析成 dbContext.Set<IEntity>() 
                            //所以需要通过反射构建指定具体类型的泛型方法
                            CallAttachEntityWithSubEntityInUseReflect(dbContext, subEntity, entityState);
                        }
                    }
                }
                //如果导航属性直接实现IEntity  【一对一关系】
                else if (typeof(IEntity).IsAssignableFrom(entityPropertyInfo.PropertyType))
                {
                    var obj = entityPropertyInfo.GetValue(entity);
                    var entityObj = obj as IEntity;
                    if (entityObj != null)
                    {
                        //附加对象，递归调用（以下方法不可用）
                        //ChangeEntityStateIncludeSubEntity(dbContext, entityObj, entityState);

                        //不直接使用AttachEntityWithSubEntity方法，而是通过反射调用的。
                        CallAttachEntityWithSubEntityInUseReflect(dbContext, entityObj, entityState);
                    }
                }
            }

            //先修改子对象，最终修改本实体对象
            ChangeEntityState(dbContext, entity, entityState, true);
        }

        /// <summary>
        /// 修改一个实体对象的状态
        /// </summary>
        /// <typeparam name="T">对象类型，必须实现IEntity接口</typeparam>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entity">需要附件跟踪的对象</param>
        /// <param name="entityState">修改对象的新状态</param>
        /// <param name="isReplaceLocalObject">如果EF上下文已经跟踪过相同Id的对象，是否替换掉本地已经被跟踪的对象</param>
        internal static void ChangeEntityState<T>(this DbContext dbContext,
            T entity, EntityState entityState, bool isReplaceLocalObject = true) where T : class, IEntity
        {
            //查询本地是否Attach跟踪指定的对象
            var localObject = dbContext.Set<T>().Local.FirstOrDefault(f => f.Id == entity.Id);

            //判断本地跟踪对象是否已经存在
            if (localObject != null)
            {
                //如果跟踪的对象一样
                if (entity == localObject)
                {
                    //判断实体类型状态是否一致，否则修改
                    if (dbContext.Entry<T>(localObject).State != entityState)
                    {
                        dbContext.Entry<T>(localObject).State = entityState;
                    }
                }
                else
                {
                    if (isReplaceLocalObject)
                    {
                        //将原有对象设置为 Detached
                        dbContext.Entry<T>(localObject).State = EntityState.Detached;
                        //附加新的对象
                        dbContext.Entry(entity).State = entityState;
                    }
                    else
                    {
                        throw new Exception(string.Format("{0}[Id：{1}] is already in context local set", entity.GetType().Name));
                    }
                }
            }
            else
            {
                //直接附加到EF上下文
                dbContext.Entry(entity).State = entityState;
            }
        }

        /// <summary>
        /// 通过反射调用本类中的 ChangeEntityStateIncludeSubEntity 方法，根据entityObj构建实际类型对应的泛型方法
        /// </summary>
        /// <param name="dbContext">EF数据库上下文</param>
        /// <param name="entityObj">需要附加跟踪的Entity对象</param>
        /// <param name="entityState">附加对象的新状态</param>
        private static void CallAttachEntityWithSubEntityInUseReflect(DbContext dbContext, IEntity entityObj, EntityState entityState)
        {
            //获取 ChangeEntityStateIncludeSubEntity 方法
            var methodInfo = typeof(DbEntityExtensions).GetMethod("ChangeEntityStateIncludeSubEntity",
                              BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
            //根据entityObj的实际类型，构建具体泛型方法
            var genericMethod = methodInfo.MakeGenericMethod(entityObj.GetType());
            //调用方法
            genericMethod.Invoke(null, new object[] { dbContext, entityObj, entityState });
        }
    }
}
